<?php
namespace \DragonU\Core;
/**
 * Transform array and object types to another object or array type.
 *
 * @internal
 * Copyright (c) 2009, Jacob Santos
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted
 * provided that the following conditions are met:
 *  * Redistributions of source code must retain the above copyright notice, this list of conditions
 *    and the following disclaimer.
 *  * Redistributions in binary form must reproduce the above copyright notice, this list of
 *    conditions and the following disclaimer in the documentation and/or other materials provided
 *    with the distribution.
 *  * Neither the name of the DragonU nor the names of its contributors may be used to endorse or
 *    promote products derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
 * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @author Jacob Santos <wordpress@santosj.name>
 * @license http://www.opensource.org/licenses/bsd-license.php New BSD license
 * @package DragonU
 * @subpackage Core
 * @since 0.1
 */

/**
 * The formatter class transforms the given type to another type.
 *
 * There are different API locations that allow for tranforming the results to
 * another type specified by the developer. This standardizes the API for those
 * locations.
 *
 * The purpose of the formatter is to transform a list of a given type to
 * another list of a given type. The class will handle raw array and raw objects
 * to transform, but not all of the transformations will be supported. Well,
 * OBJECT_K will not be supported when given a single array or object.
 *
 * @package DragonU
 * @subpackage Core
 * @since 0.1
 */
class Formatter
{
	/**
	 * Transform to object type.
	 *
	 * The array indices will be numeric for the rows.
	 *
	 * @since 0.1
	 * @var int
	 */
	const OBJECT = 1;

	/**
	 * Transform to object type.
	 *
	 * The array indices for the rows will have their key the name of the first
	 * object attribute. This will reduce the amount of rows returned.
	 *
	 * @since 0.1
	 * @var int
	 */
	const OBJECT_K = 2;

	/**
	 * Transform to numeric array type.
	 *
	 * @since 0.1
	 * @var int
	 */
	const ARRAY_N = 4;

	/**
	 * Transform to named array type.
	 *
	 * @since 0.1
	 * @var int
	 */
	const ARRAY_A = 8;

	/**
	 * Holds the raw data before it is transformed.
	 *
	 * @since 0.1
	 * @var object|array
	 */
	protected $raw = null;

	/**
	 * Stores the raw data for transformations or passing through.
	 *
	 * It should be expected that those using this class will not be doing any
	 * checking for types for passing through and even, if they were, we should
	 * still assume to pass through the raw data in the event that we can't do
	 * the transformations.
	 *
	 * @since 0.1
	 *
	 * @param mixed $raw
	 */
	public function __construct($raw = null)
	{
		$this->raw = $raw;
	}

	/**
	 * Transforms one type to another.
	 *
	 * @since 0.1
	 *
	 * @param $type Optional, default is OBJECT. Use class constants for type.
	 * @return mixed Same type, if not PDOStatement, object, or array. On success will be the same type specified in $type.
	 */
	public function to($type = self::OBJECT)
	{
		if ( $this->raw instanceof \PDOStatement )
		{
			return $this->transformPdo($type);
		}

		if ( $this->isMultiArray() )
		{
			if ( is_array( $this->raw ) )
			{
				return $this->transformArray($type);
			}

			if ( is_object( $this->raw ) )
			{
				return $this->transformObject($type);
			}
		}

		return $this->transformSingle($type);
	}

	protected function transformSingle($to)
	{
		if ( is_array($this->raw) )
		{
			switch( $to )
			{
				case self::OBJECT:
					return (object) $this->raw;

				case self::ARRAY_N:
					return array_values( $this->raw );

				case self::ARRAY_A:
				default:
					return $this->raw;
			}
		}
		else if ( is_object($this->raw) )
		{
			switch( $to )
			{
				case self::ARRAY_A:
					return (array) $this->raw;

				case self::ARRAY_N:
					return array_values( get_object_vars($this->raw) );

				case self::OBJECT:
				default:
					return $this->raw;
			}
		}

		return $this->raw;
	}

	/**
	 * Whether or not the items in the array are arrays or objects.
	 *
	 * The purpose of this is that DB results are arrays of arrays or arrays of
	 * objects, and array of arrays and array of objects need to be handled
	 * differently than just single array of values and single object.
	 *
	 * @since 0.1
	 * @todo Check last and middle to ensure that entire array is of arrays and
	 * objects.
	 *
	 * @return bool True, if values contains array or object.
	 */
	protected function isMultiArray()
	{
		if ( is_array( $this->raw ) && ! empty( $this->raw ) )
		{
			$values = array_values($this->raw);

			if ( is_array( $values[0] ) || is_object( $values[0] ) )
			{
				return true;
			}
		}

		return false;
	}

	/**
	 * Transform raw multi-array to OBJECT_K format.
	 *
	 * @since 0.1
	 *
	 * @return array Processed array or raw, if doesn't need to be processed.
	 */
	protected function transformToObjectK()
	{
		if ( $this->isMultiArray() )
		{
			$processed = array();

			foreach( $this->raw as $item )
			{
				if( is_array($item) )
				{
					if( ! isset( $processed[ $item[0] ] ) )
					{
						$tranformed = (object) $item;
						$processed[ $item[0] ] = $transformed;
					}
				}
				else
				{
					$keys = array_keys( get_object_vars($item) );

					if( ! isset( $processed[ $keys[0] ] ) )
					{
						$processed[ $keys[0] ] = $item;
					}
				}
			}

			return $processed;
		}

		return $this->raw;
	}

	protected function transformArray($type)
	{
		switch( $type )
		{
			case self::OBJECT:
				return (object) $this->raw;

			case self::OBJECT_K:
				return $this->transformToObjectK();

			case self::ARRAY_N:
				$processed = array();

				foreach( $this->raw as $item )
				{
					$processed[] = array_values( $item );
				}

				return $processed;

			case self::ARRAY_A:
			default:
				return $this->raw;
		}
	}

	/**
	 * Transform object to specified type.
	 *
	 * We only support objects and arrays, so if we are given an object and then
	 * asked to give an object, there is no reason to do any transformations.
	 * The overhead then, is that
	 */
	protected function transformObject($type)
	{
		switch( $type )
		{
			case self::OBJECT_K:
				return $this->transformToObjectK();

			case self::ARRAY_N:
				$processed = array();

				foreach( $this->raw as $item )
				{
					$processed[] = array_values( get_object_vars($item) );
				}

				return $processed;

			case self::ARRAY_A:
				return (array) $this->raw;

			case self::OBJECT: // Raw is already the type we want.
			default:
				return $this->raw;
		}
	}

	/**
	 * PDO supports all of the types and faster than the PHP algorithms.
	 *
	 * We use the PDO equivalents, because doing the transformations in C/C++ is
	 * faster than doing it in PHP, so it is better optimized when doing
	 * queries.
	 *
	 * There are a few quirks with using PDO::FETCH_COLUMN|PDO::FETCH_GROUP, so
	 * it might mean that for that, we have to do it in PHP instead of using
	 * PDO.
	 *
	 * The return type will never be PDO object, and will always return the
	 * results. This is to ensure that the default behavior of WordPress is
	 * kept. Also, it doesn't make any sense to return PDO object when the
	 * results are asked.
	 *
	 * @since 0.1
	 *
	 * @param int $type Formatter constant type.
	 * @return object|array
	 */
	protected function transformPdo($type)
	{
		switch( $type )
		{
			case self::OBJECT_K:
				return $this->raw->fetchAll(PDO::FETCH_COLUMN|PDO::FETCH_GROUP, 0);
			case self::ARRAY_N:
				return $this->raw->fetchAll(PDO::FETCH_NUM);
			case self::ARRAY_A:
				return $this->raw->fetchAll(PDO::FETCH_ASSOC);
			case self::OBJECT:
			default:
				return $this->raw->fetchAll(PDO::FETCH_OBJ);
		}
	}

}